﻿/// <reference path="jquery-1.7.1.js" />

//////////////////////////////////////////////////////////////////////////
// AralNovel Script class
//////////////////////////////////////////////////////////////////////////

// Top-level command class
function ANScript(_novel) {
	this.novel = _novel;
	this.info = {};
	this.rootinfo = null;
	this.macros = {};

	this.labels = {};
	this.commandList = [];
	this.curOffset = 0;
	this.waiting = null;

	var _this = this;
	var _timerForMainLoop = undefined;
	var _isPaused = false;
	

	//////////////////////////////////////////////////////////////////////////
	// Jump script offset to the label / url
	//////////////////////////////////////////////////////////////////////////
	this.jump = function (urlOrLabel, strPart, fnCallback)
	{
		// 1. Find from labels collection
		if(_this.labels[urlOrLabel] != undefined)
		{
			_this.stop();
			_this.curOffset = _this.labels[urlOrLabel];
			_this.run();		
		}
		// 2. Instant command
		else if(/^\[.+\]$/.test(urlOrLabel))
		{
			var tmpCmd = parseANCommand(urlOrLabel);
			this.executeCommand(tmpCmd);
			var _timerForTmpCmd = setInterval(function(){
				if(tmpCmd.isCompleted()) {
					if(fnCallback) fnCallback();
					clearInterval(_timerForTmpCmd);
				}
			}, 30);
		}
		// 3. Treat argment as url
		else
		{

			// Stop
			this.stop();
			this.waiting = {
				title : _this.info.title,
				part: strPart,
				callback: fnCallback
			};

			// Get HTML
			$.get(urlOrLabel, function (data) {

				var loadedUri = parseUri(urlOrLabel);

				// Convert to ANScript
				var arrScripts = convertHtmlToANScript(data);

				// Check loop
				for (var i = 0; i < arrScripts.length; i++) {

					// Parse #aralnovelbegin directive
					var tp = {title:loadedUri.authority, part: ""};
					var strToken = null;

					var matches = arrScripts[i].match(/#aralnovelbegin\s+\"(.*?)\".*/i);
					if (matches && matches.length > 1) {
						tp.title = matches[1].trim();
					}
					matches = arrScripts[i].match(/#aralnovelbegin\s+\"(?:.*?)\"\s*?,\s*?\"(.*?)\".*/i);
					if (matches && matches.length > 1) {
						tp.part = matches[1].trim();
					}

					// Check title and part
					if(_this.waiting.title != undefined && _this.waiting.title != tp.title) continue;
					if(_this.waiting.part != undefined && _this.waiting.part != tp.part) continue;

					// Post ANScript
					_this.postANScript(arrScripts[i], loadedUri.relative);					
					return;
				}

				// If no script, try to get the script using IFRAME
				_this.novel.iframe.attr("src", urlOrLabel);
					
				/*
				var arrFrameTags = data.match(/\<(?:frame|iframe)\s+[^\<]*?src=['"]?([^'"\<\>]+?)['"]?\s+.*?\>/gim);
				if(arrFrameTags != null && arrFrameTags.length > 0) {
					for(var i=0; i < arrFrameTags.length; i++) {
						var frameSrc = arrFrameTags[i].replace(/\<(?:frame|iframe)\s+[^\<]*?src=['"]?([^'"\<\>]+?)['"]?\s+.*?\>/i, "$1");
						if(frameSrc && frameSrc != "") tryParseRecursive(frameSrc);
					}
				}
				else if(bShowErr == true) {
					alert("Cannot find the script from " + urlOrLabel);
				}
				*/

				return;			
			});
							

		} // end of else
	} // end of this.jump

	//////////////////////////////////////////////////////////////////////////
	// Run / Stop the script main loop
	//////////////////////////////////////////////////////////////////////////
	this.postANScript = function (strScript, strUrlFrom)
	{
		// Parse script
		_this.labels = {};
		_this.commandList = [];
		_this.curOffset = 0;
		_this.parseANScript(strScript);
		if(strUrlFrom) _this.info.url = strUrlFrom;

		// Initialize CSS of layers
		_this.novel.resetLayers();

		// Callback function
		if(_this.waiting && _this.waiting.callback != undefined) _this.waiting.callback();
				
		// Run
		_this.waiting = null;			
		_this.run();
	}

	//////////////////////////////////////////////////////////////////////////
	// Run / Stop the script main loop
	//////////////////////////////////////////////////////////////////////////
	this.run = function ()
	{
		if(this.waiting && this.waiting.title) return;
		_isPaused = true;

		// Setup keyboard/mouse handler

		// Load offset from local storage


		// Check End of command list
		if(_this.curOffset >= _this.commandList.length){
			_this.stop();
			return;
		}

		// Execute command of the current offset
		_this.executeCommand(_this.commandList[_this.curOffset]);
		_timerForMainLoop = setInterval(mainloop, 30);

		_isPaused = false;
	}
	this.stop = function ()
	{
		if(this.waiting && this.waiting.title) return;
		_isPaused = false;

		// Stop Main timer
		clearInterval(_timerForMainLoop);
		_timerForMainLoop = undefined;

		// Finish the current command
		if(_this.curOffset < _this.commandList.length && _this.commandList[_this.curOffset].isCompleted() == false)
		{
			_this.commandList[_this.curOffset].onSkip();
		}

		_isPaused = true;
	}

	//////////////////////////////////////////////////////////////////////////
	// Pause / Resume the gameplay
	//////////////////////////////////////////////////////////////////////////
	this.pause = function ()
	{
		if(this.waiting && this.waiting.title) return;
		_isPaused = true;
	}
	this.resume = function ()
	{
		if(this.waiting && this.waiting.title) return;
		_isPaused = false;
	}
	this.isPaused = function ()
	{
		return (this.waiting != null || _isPaused || this.novel.sysmenu.isShowing());
	}
		
	//////////////////////////////////////////////////////////////////////////
	// Execute a command manually
	//////////////////////////////////////////////////////////////////////////
	this.executeCommand = function (cmd)
	{
		cmd.onStart();
	}
	
	//////////////////////////////////////////////////////////////////////////////////////////////
	// Parse a pure AN script 
	// (Not HTML parsing, it assumes a script arg already have been converted through the function 'convertHtmlToANScript')
	this.parseANScript = function (textScript) {
			
		var ctrlChars = ['#','[', '\''];
		var line = 0, col = 0, offset = 0;
		var textBuf = "";

		try {
				
			// Parsing
			for(var offset = 0; offset < textScript.length; offset++)
			{
				var ch = textScript.charAt(offset);

				if(ctrlChars.contains(ch))
				{
					// add Text cmd 
					if(textBuf.trim().replace(/\r/g,"").replace(/\n/g,"").length > 0)
					{
						newCmd = new ANCmd_text(_this.novel, _this.wrapCommandArgument(textBuf));
						_this.commandList.push(newCmd);
					}
					textBuf = "";

					if(ch === '#')	// Preprocessor statement
					{
						var strLine = "" + ch;
						var bCommentMode = false;
						while(offset < textScript.length && ch != '\n')
						{
							offset++;
							ch = textScript.charAt(offset);
							if(ch === '\'') bCommentMode = true;
							if(ch != '\r' && ch != '\n' && !bCommentMode) strLine += ch;
						}							
						var bRes = preprocess(strLine);
						if(bRes === false) break;
					}
					else if(ch === '\'')	// Comment
					{
						//var strLine = "" + ch;
						while(offset < textScript.length && ch != '\r' && ch != '\n')
						{
							offset++;
							ch = textScript.charAt(offset);
							if(ch == '\r' && offset+1 < textScript.length && textScript.charAt(offset+1) == '\n') {
								offset++;
							}
						}							
						// do nothing							
					}
					else if(ch === '[')	// Command
					{
						try
						{
							var eidx = endIndexOf(textScript, offset);							
							if(eidx <= offset) throw "cannot find closing bracket ']'";

							// Check extendability
							var strCommand = textScript.substr(offset, eidx-offset+1);
							strExtendedCommand = extendCommand(strCommand);
								
							if(strExtendedCommand != strCommand)	// If something changed due to #define
							{
								// Recursive call for extended commands
								//var subResult = _this.parseANScript(strExtendedCommand);
								//if(subResult && subResult.length > 0)
								//{
								//	_this.commandList = _this.commandList.concat(subResult);
								//}
								_this.parseANScript(strExtendedCommand);
							}
							else // Single command
							{
								newCmd = parseANCommand(strCommand);
								if(!newCmd) throw ("invalid command " + strCommand);
								_this.commandList.push(newCmd);																		
							}

							offset = eidx;
							
						} catch (parsingEx) {
							textBuf += ch;								
						}
					}// end of (ch === '[')
				}
				else
				{
					textBuf += ch;
				}
			}// end of for

			// add Text cmd 
			if(textBuf.trim().replace(/\r/g,"").replace(/\n/g,"").length > 0)
			{
				newCmd = new ANCmd_text(_this.novel, _this.wrapCommandArgument(textBuf));
				_this.commandList.push(newCmd);
			}
			textBuf = "";
		}
		catch (ex) {
			throw ex;
		}

	}// end of parseHtmlScript


	//////////////////////////////////////////////////////////////////////////////////////////////
	// wrapCommandArgument
	this.wrapCommandArgument = function (strArg)
	{
		if(typeof(strArg) == "string") {
			return "\"" + strArg.replace(/\\/gm,"\\\\").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/'/g,"\\\'").replace(/"/g,"\\\"") + "\"";
		}
		else
		{
			return strArg + "";
		}
	}

		
	//////////////////////////////////////////////////////////////////////////////////////////////
	// Returns a matching index of the given openning braket character
	function endIndexOf(text, offset)
	{
		var brackets = {
			'(':')', '[':']', '{':'}', '<':'>', '\'':'\'', '\"':'\"'
		};

		var chToFind = brackets[text.charAt(offset)];
		if(chToFind)
		{
			for(var curIdx = offset+1; curIdx < text.length; curIdx++)
			{
				curCh = text.charAt(curIdx);
				if(curCh === chToFind)
				{
					return curIdx;
				}
				else if((chToFind === '\'' || chToFind === '\"') && curCh === '\\')
				{
					curIdx++;
				}
				else if(brackets[curCh])
				{
					curIdx = endIndexOf(text, curIdx);
					if(curIdx == -1) break;
				}
			}
		}

		return -1;
	}


	//////////////////////////////////////////////////////////////////////////////////////////////
	// Convert a text command to a ANCommand object
	function tokenize(str)
	{
		function trimEvenNewLine(strToTrim) {
			return ( strToTrim.replace(/^[\s\r\n\xA0]+/,"").replace(/[\s\r\n\xA0]+$/,"") );
		};
				
		var arrTokens = [];
		var separators = [' ','\t','\r','\n'];
		var prevIdx = 0;
		for(var i=0; i<str.length; i++)
		{
			var ch = str.charAt(i);
			if(separators.contains(ch))
			{
				if(arrTokens.length > 0)
				{
					arrTokens.push(trimEvenNewLine(str.substr(prevIdx, i-prevIdx)));
				}
				else if(prevIdx < i)
				{
					arrTokens.push(trimEvenNewLine(str.substr(prevIdx, i-prevIdx)));
					separators = [','];
				}
				prevIdx = i + 1;
			}
			else if(ch == '\'' || ch == '\"')
			{
				i = endIndexOf(str, i);
				if(i == -1) throw ch + " qoutation mark mismatched!";
			}
			else if(ch == '(')
			{
				i = endIndexOf(str, i);
				if(i == -1) throw "Closing braket \')\' mismatched!";
			}
		}

		if(prevIdx < str.length) arrTokens.push(trimEvenNewLine(str.substr(prevIdx, str.length-prevIdx)));

		return arrTokens;
	}		


	//////////////////////////////////////////////////////////////////////////////////////////////
	// Convert a text command to a ANCommand object
	function parseANCommand(strCommand)
	{
		// Script command validation check
		if(!strCommand || strCommand.length < 2 || strCommand.charAt(0) !== '[' || strCommand.charAt(strCommand.length-1) !== ']')
			throw "Cannot parse command : " + strCommand;
			
		// Check it is inline-javascript [{ ... }]
		if(strCommand.length >= 4 && strCommand.charAt(1) === '{' && strCommand.charAt(strCommand.length-2) === '}')
		{
			//strCommand = "[javascript " 
			//			+  _this.wrapCommandArgument(strCommand.substr(2, strCommand.length - 4).trim())
			//			+ "]";
			return (new ANCmd_javascript(_this.novel, strCommand.substr(2, strCommand.length - 4).trim()));
		}
		
		// Tokenization
		strCommand = strCommand.substr(1, strCommand.length-2);
		var tokens = tokenize(strCommand);
		if(!tokens || tokens.length == 0) 
			throw "Tokenization error : " + strCommand;
			
		// Create ANCommand object
		var cmdName = tokens.shift();
			
		// Check it's an async command
		var isAsync = false;
		if(cmdName.charAt(cmdName.length-1) == '+')
		{
			cmdName = cmdName.substring(0, cmdName.length-1);
			isAsync = true;
		}

		// Create a command object with given parameters
		if(!eval("ANCmd_" + cmdName))
			throw "Unknown command name : " + cmdName;
			
		var strTmpScript = "new ANCmd_" + cmdName + "(_this.novel";
		for(var i=0; i<tokens.length; i++)
		{
			strTmpScript += (", tokens[" + i + "]");
		}
		strTmpScript += ")";

		var cmd = eval(strTmpScript);
		cmd.isAsync = isAsync;
		return cmd;
	}

	/*
	function getTitleAndPart(strToken) {
		var firstArg = strToken.trim().replace(/^\"/, "").replace(/\"$/, "");
		var retInfo = {title:firstArg, part:""};

		var matches = firstArg.match(/(.*)#(.*?)$/);
		if(matches != null && matches.length == 3) {
			retInfo.title = matches[1].trim();
			retInfo.part = matches[2].trim();
		}

		return retInfo;
	}
	*/
	
	//////////////////////////////////////////////////////////////////////////////////////////////
	// Pre-process a line starts with prefix '#'
	function preprocess(strLine)
	{
		//var arrTokens = strLine.split(/[ \t]+/g);
		var arrTokens = tokenize(strLine);
		if(arrTokens[0] == "#aralnovelbegin")
		{
			var loadedUri = parseUri(location.toString());

			// Title and Part
			_this.info.title = loadedUri.authority;
			_this.info.part = "";
			if(arrTokens.length > 1) {
				_this.info.title = arrTokens[1].trim().replace(/^\"/, "").replace(/\"$/, "").trim();
			}	
			if(arrTokens.length > 2) {
				_this.info.part = arrTokens[2].trim().replace(/^\"/, "").replace(/\"$/, "").trim();
			}	


			// Current url
			_this.info.url = loadedUri.relative;
			
			// Viewport Size
			_this.info.viewportWidth = (arrTokens.length > 3) ? arrTokens[3] : 640;
			_this.info.viewportHeight = (arrTokens.length > 4) ? arrTokens[4] : 480;
			$(_this.novel.viewportElement).css("width", _this.info.viewportWidth);
			$(_this.novel.viewportElement).css("height", _this.info.viewportHeight);
			
			// Root information
			if(_this.rootinfo == null) _this.rootinfo = $.extend(true, {}, _this.info);

			return true;
		}
		else if(arrTokens[0] == "#label")
		{
			if(arrTokens.length > 2)
				throw "Wrong directive expression of #label - " + arrTokens.slice(2, arrTokens.length-1).join(' ');
			_this.labels[arrTokens[1].replace(/[\"\']/g,'')] = _this.commandList.length;
			return true;
		}
		else if(arrTokens[0] == "#macro")
		{
			if(arrTokens.length < 2) return true; //throw "Wrong directive expression - " + strLine;
			
			var strTmp = strLine.substr(7).trim();
			if(strTmp.length == 0 || strTmp.charAt(0) != '[') return true; //throw "Wrong directive expression - " + strLine;

			var eidx = endIndexOf(strTmp, 0);
			if(eidx <= 1) return true; //throw "cannot find closing bracket ']'";

			// Extract macro name
			var strMacroName = strTmp.substr(1, eidx-1);
			_this.macros[strMacroName] = (eidx+1 < strTmp.length ? strTmp.substr(eidx+1) : "");

			return true;
		}
		else if(arrTokens[0] == "#aralnovelend")
		{
			return false;
		}

	}

	function extendCommand(strCommand)
	{
		var strRetVal = strCommand;

		// Tokenization
		strCommand = strCommand.substr(1, strCommand.length-2);
		var tokens = tokenize(strCommand);

		if(tokens && tokens.length > 0)
		{
			// Get macro name
			var macroName = tokens[0];
			
			if(_this.macros[macroName] != undefined)
			{
				strRetVal = _this.macros[macroName];
				
				for(var i=1; eval("/%" + i + "[^\d]/g").test(strRetVal); i++)
				{
					strRetVal = strRetVal.replace(eval("/%" + i + "/g"), (i < tokens.length ? tokens[i] : "undefined"));
				}
			}
		}

		return strRetVal;
	}

		


	function mainloop()
	{
		// Fetch
		var cmd = _this.commandList[_this.curOffset];			
		if(cmd == undefined || _this.isPaused()) return;

		if(cmd.isCompleted()) {
			var bIsFailed;
			
			do
			{
				bIsFailed = false;
				_this.curOffset++;
				if(_this.curOffset < _this.commandList.length)
				{
					try {
						// Execute next command
						_this.executeCommand(_this.commandList[_this.curOffset]);
					} catch (ex) {
						bIsFailed = true;
					}
				}
				else
				{
					// Game over
					//clearInterval(_this.timerForMainLoop);
					_this.stop();
				}
			} while (_this.curOffset < _this.commandList.length && (_this.commandList[_this.curOffset].isAsync || _this.commandList[_this.curOffset].isCompleted() || bIsFailed)); // Skip completion checking about async commands
		}
		else if(_this.novel.input.keyboard['ctrl'] == true && _this.novel.input.keyboard['shift'] == true) {
			cmd.onSkip();
		}
	}
}



/////////////////////////////////////////////////////////////////////////////////
// convertHtmlToANScript
function convertHtmlToANScript(strHTML) {
	//var inputText = document.getElementById("input").value;
	var returnText = "" + strHTML;

	//-- remove BR,P tags and replace them with line break 
	//returnText = returnText.replace(/<br>/gi, "\n");
	//returnText = returnText.replace(/<br\/>/gi, "\n");
	returnText = returnText.replace(/<br.*?>/gi, "\n");
	returnText = returnText.replace(/<p.*?>/gi, "\n");
	returnText = returnText.replace(/<\/p>/gi, "\n");
	returnText = returnText.replace(/<div.*?>/gi, "\n");
	returnText = returnText.replace(/<\/div>/gi, "\n");

	//-- Convert A tags to #label
	returnText = returnText.replace(/<a\s+.*name="(.*?)"(?:.*?)>/gi, "#label $1");

	//-- Convert IMG tags to [bg] command
	returnText = returnText.replace(/<img\s+.*src="(.*?)"(?:.*?)>/gi, "[bg \"$1\"]");

	//-- Convert HR tags to [wait][clear] command
	returnText = returnText.replace(/<hr.*(?:.*?)>/gi, "[wait][clear]");

	//-- remove all inside SCRIPT and STYLE tags 
	returnText = returnText.replace(/<script.*>[.\n]*?<\/\s*script>/gi, "");
	returnText = returnText.replace(/<style.*>[.\n]*?<\/\sstyle>/gi, "");

	//-- remove all else 
	returnText = returnText.replace(/<(\s+?)/g, "&lt;$1");
	returnText = returnText.replace(/<(?:.|\s)*?>/g, "");

	//-- get rid of more than 2 multiple line breaks: 
	returnText = returnText.replace(/(?:(?:\r\n|\r|\n)\s*){2,}/gim, "\r\n");

	//-- get rid of more than 2 spaces: 
	returnText = returnText.replace(/ +(?= )/g, '');

	//-- get rid of html-encoded characters: 
	returnText = returnText.replace(/&nbsp;/gi, " ");
	returnText = returnText.replace(/&amp;/gi, "&");
	returnText = returnText.replace(/&quot;/gi, '"');
	returnText = returnText.replace(/&lt;/gi, '<');
	returnText = returnText.replace(/&gt;/gi, '>');

	// Correction
	returnText = returnText.replace(/[\s\r\n]+?(\[wait\]\[clear\])/g, "[wait][clear]"); 	// Remove a new line just before '[wait][clear]'
	returnText = returnText.replace(/(\[[\r\n\s]*?setimg[^\[\]]+?)\[bg (\".*?\")\]([^\[\]]*?\])/g, "$1$2$3"); // Allow to use img tag as a command argument.

	var arrScripts = returnText.match(/^\s*(#aralnovelbegin)([^\r\n]|[\r\n])*?^(#aralnovelend)\s*$/gim);

	//-- return 
	if (arrScripts == null) arrScripts = [];
	return arrScripts;
}

//